package asteroids.model;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.*;
import javax.swing.Timer;

import be.kuleuven.cs.som.annotate.*;
import asteroids.CollisionListener;
import asteroids.Util;


/**
 * A class of worlds, which consist of a rectangular playing field and a list of flying objects.
 *  
 * @invar	Each world must have a proper list of flyingObjects.
 * 			| hasProperFlyingObjects()
 * @invar	Each world must have a proper width.
 * 			| isValidSize(width)
 * @invar	Each world must have a proper height.
 * 			| isValidSize(height)
 * 
 * @version 1.0
 * 
 * @author Thijs Peirelinck & Wouter Parys
 *
 */
public class World {
	private List<FlyingObject> flyingObjects;
	private final double width;
	private final double height;
	private Timer actionTimer;
	boolean timerIsStarted;
	
	/**
	 * Initialize this new world with a given width and height.
	 * 
	 * @param 	width
	 * 			the width for this new world.
	 * @param 	height
	 * 			the height for this new world.
	 * @throws	IllegalArgumentException
	 * 			The given width is not a valid size for World.
	 * 			| !isValidSize(width)
	 * @throws	IllegalArgumentException
	 * 			The given height is not a valid size for World.
	 * 			| !isValidSize(height)
	 * @post	The width of this new World is equal to the given width.
	 * 			| this.width == width
	 * @post	The height of this new World is equal to the given height.
	 * 			| this.height == height
	 * 
	 */
	public World(double width, double height) throws IllegalArgumentException {
		if(! isValidSize(width)) 
			throw new IllegalArgumentException();
		if(! isValidSize(height)) 
			throw new IllegalArgumentException();
		this.width = width;
		this.height = height;
		flyingObjects = new ArrayList<FlyingObject>();
	}

	@Basic @Raw
	public double getWidth() {
		return width;
	}
	
	@Basic @Raw
	public double getHeight() {
		return height;
	}
	
	/**
	 * Check whether the new size of the world is possible.
	 * 
	 * @param	size
	 * 			The size to check.
	 * @return	true if and only if the given coordinate is in the range of the class Double.
	 * 			| (size > 0 && size <= Double.MAX_VALUE)
	 * 
	 */
	@Model
	private boolean isValidSize(double size) {
		return (size > 0 && size <= Double.MAX_VALUE);
	}
	
	
	/**
	 * Return the flyingObject that overlaps with a given object.
	 * 
	 * @param	object
	 * 			the flyingObject that is getting checked for an overlapping flyingObject.
	 * @return	A list of the flyingObjects that overlap with the object to check.
	 * 			| for each flyingObject in flyingObjects
	 * 			| if (flyingObject.overlap(object)
	 * 			| 	then overlappingObjects.add(flyingObject)
	 * 			| result == overlappingObjects 			
	 * 			
	 */
	public List<FlyingObject> getOverlapObjects(FlyingObject object) {
		ArrayList<FlyingObject> overlappingObjects = new ArrayList<FlyingObject>();
		for (FlyingObject flyingObject : flyingObjects) {
			if (flyingObject.overlap(object)) {
				overlappingObjects.add(flyingObject);
			}
		}
		return overlappingObjects;
	}
	
	/**
	 * Add a new flyingObject to flyingObjects.
	 * 
	 * @param	object
	 * 			The flyingObject to add.
	 * @throws	IllegalArgumentException
	 * 			The object that needs to be add does not fit in the World.
	 * 			| !canHaveAsFlyingObject(object)
	 * @post	The object is added to flyingObjects.
	 * 			| flyingObjects.contains(object)
	 * @post	This world is the new world for the object.
	 * 			| object.getWorld == this
	 */
	public void addFlyingObject(FlyingObject object) throws IllegalArgumentException {
		if(! canHaveAsFlyingObject(object)) {
			throw new IllegalArgumentException();
		}
		flyingObjects.add(object);
		object.setWorld(this);
	}
	
	/**
	 * Take care of an Bullet that can't be fired in the world because the bullet can not be added to flyingObjects.
	 * @param	bullet
	 * 			The bullet that can't be fired.
	 * @effect	if the bullet is out of this world the bullet is therminated
	 * 			| if(!fitInWorld(bullet)) then 
	 * 			|	bullet.terminate()
	 * @effect	When the reason, that the bullet can not be fired, is that the bullet overlaps with one or more
	 * 			flyingObjects in the world. The bullet collides immediately with these flyingObjects.
	 * 			| if (this.overlapWith)
	 * 			|	then for each flyingObject in this.getOverlapObjects(bullet):
	 * 			|		flyingObject.terminate()
	 * 			|	bullet.terminate()
	 */
	public void solveImpossibleBullet(Bullet bullet) {
		if (!fitInWorld(bullet))
			bullet.terminate();
		if (this.overlapWith(bullet)) {
			for (FlyingObject overlapObject : this.getOverlapObjects(bullet)) {
				overlapObject.terminate();
				bullet.terminate();
			}
		}
	}
	
	/**
	 * Check whether the world can have the given flyingObject.
	 * 
	 * @param	object
	 * 			The flyingObject that needs to be checked.
	 * @return	True if and only if the object doesn't already has a world and the object fits in the world 
	 * 			and the objects doesn't overlap with other flyingObjects in the world and the objects isn't null
	 * 			and the object is not terminated.
	 * 			| result == ((!object.hasWorld()) 
	 * 			|			&& fitInWorld(object) 
	 * 			|			&& (!overlapWith(object)) 
	 * 			|			&& object != null 
	 * 			|			&& !object.isTerminated())
	 */
	@Model
	public boolean canHaveAsFlyingObject(FlyingObject object) {
		return (object != null && (!object.hasWorld()) && fitInWorld(object) && (!overlapWith(object)) && !object.isTerminated());
		
	}
	
	/**
	 * Check whether the given flyingObject fits in the world.
	 * 
	 * @param	object
	 * 			The flyingObject that needs to be checked.
	 * @return	True if and only if the objects fits in the world.
	 * 			| result == (object.getPositionX() >= object.getRadius() 
	 * 			|		&& object.getPositionX()+object.getRadius() <= this.getWidth()
	 * 			|		&& object.getPositionY() >= object.getRadius()
	 * 			|		&& object.getPositionY()+object.getRadius() <= this.getHeight())

	 */
	@Model
	private boolean fitInWorld(FlyingObject object) {
		if(object.getPositionX() >= object.getRadius() 
				&& object.getPositionX()+object.getRadius() <= this.getWidth()
				&& object.getPositionY() >= object.getRadius()
				&& object.getPositionY()+object.getRadius() <= this.getHeight()) {
			return true;
		}
			return false;
	}
	
	/**
	 * Check whether the given flyingObject overlaps with another flyingObject in this world.
	 * 
	 * @param	object
	 * 			The flyingObject to check.
	 * @return	True if and only if the object doesn't overlap with any flyingObject that is currently in this world.
	 * 			| if (for each flyingObject in flyingObjects:
	 * 			|		(flyingObject instanceof Bullet && flyingObject.getSource == object) || flyingObject.overlap(object) == false
	 * 			|		)
	 * 			|	then result == false
	 * 			| else
	 * 			|	result == true
	 * 			|		
	 * @note	A bullet that overlaps his own mothership will intentionally be marked as not overlapping, in this method.
	 */
	@Model
	private boolean overlapWith(FlyingObject object) {
		for(FlyingObject flyingObject: flyingObjects) {
			boolean overlap = flyingObject.overlap(object);
			if(overlap == true) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Remove a flyingObject from flyingObjects based on an index-number.
	 * 
	 * @param	index
	 * 			The index of the flyingObject that needs to be removed.
	 * @throws	IndexOutOfBoundsException
	 * 			The index is to big for the current size of flyingObjects.
	 * 			| index >= flyingObjects.size()
	 * @post	The world of the flyingObject to which the index refers is set to null.
	 * 			| flyingObjects.get(index).getWorld == null
	 * @effect	The flying object to which the index refers will be removed from flyingObjects.
	 * 			| flyingObjects.remove(index)
	 *  			
	 */
	public void removeFlyingObject(int index) throws IndexOutOfBoundsException {
		flyingObjects.get(index).setWorld(null);
		flyingObjects.remove(index);
	}
	
	/**
	 * Remove a flyingObject from flyingObjects based on a given flyingObject.
	 * 
	 * @param	object
	 * 			The flyingObject that needs to be removed.
	 * @throws	IllegalArgumentException
	 * 			The object is null.
	 * 			| object == null
	 * @post	The object is removed from flyingObjects.
	 * 			| !flyingObjects.contains(object)
	 * @post	The world of the object is set to null.
	 * 			| flyingObjects.get(index).getWorld == null
	 */
	public void removeFlyingObject(FlyingObject object) throws IllegalArgumentException {
		if(object == null) {
			throw new IllegalArgumentException();
		}
		else {
			List<FlyingObject> list = new ArrayList<FlyingObject>();
			list = this.getFlyingObjects();
			list.remove(object);
			object.setWorld(null);
			this.flyingObjects = list;
		}
	}
	
	/**
	 * Check whether flyingObjects is consistent.
	 * 
	 * @return	true if and only if all objects in flyingObjects fit togheter in the world.
	 * 			| if (for each flyingObject in flyingObjects:
	 * 			| 		canHaveAsFlyingObject(flyingObject) == true)
	 * 			| 	then result == true
	 * 			| else
	 * 			|	result == false
	 */
	public boolean hasProperFlyingObject() {
		for(int i = 0; i < flyingObjects.size(); i++) {
			if(!canHaveAsFlyingObject(flyingObjects.get(i)))
				return false;
		}
		return true;
	}
	
	
	/**
	 * Advance the time in the world by a given dt.
	 * 
	 * @param	dt
	 * 			The time the world has to advance.
	 * @param	collisionListener
	 * 			The interface that takes care of a potential collision.
	 * @effect	If a collide occurs in a time smaller than dt, the world evolves with the time till the
	 * 			smallest collision. If the first collision is a collision of a flyingObject with a wall,
	 * 			first all flyingObjects in this world will move with the time till this collision. Hereafter
	 * 			the collision is solved by letting the flyingObject that was going to collide first,
	 * 			bounce off the wall. On the other hand, when the first collision is a collision of two
	 * 			flyingObjects, all the flyingObjects in the world will move to that time and hereafter the
	 * 			collision of the two flyingObjects will be solved.
	 * 			After this first collide is solved, the method evolve will be reevoked with a new dt. This 
	 * 			new dt is equal to the previous dt subtracted with the time the world has moved, till the 
	 * 			previous collision.
	 * 			| if (timeTillObjectCollision() <= dt || timeTillBoundaryCollision() <= dt)
	 * 			|	then
	 * 			|		if ((timeTillObjectCollision() <= dt && timeTillObjectCollision() == timeTillBoundaryCollision())
	 * 			|			then for each flyingObject in flyingObjects:
	 * 			|					flyingObject.move(timeTillObjectCollision())
	 * 			|				 getFirstObjectCollision()[0].collide(getFirstObjectCollision()[2]
	 * 			|				 getBoundaryObject().bounceOffWall()
	 * 			|			 	 dt = dt - timeTillObjectCollision()
	 * 			| 		if ((timeTillObjectCollision() <= dt || timeTillBoundaryCollision() <= dt) && 
	 * 			|			 timeTillObjectCollision() < timeTillBoundaryCollision())
	 * 			|			then for each flyingObject in flyingObjects:
	 * 			|					flyingObject.move(timeTillObjectCollision())
	 * 			|				 getFirstObjectCollision()[0].collide(getFirstObjectCollision()[2]
	 * 			|			 	 dt = dt - timeTillObjectCollision()
	 * 			| 		if ((timeTillObjectCollision() <= dt || timeTillBoundaryCollision() <= dt) && 
	 * 			|			 timeTillObjectCollision() > timeTillBoundaryCollision())
	 * 			|			then for each flyingObject in flyingObjects:
	 * 			|			 		flyingObject.move(timeTillBoundaryCollision())
	 * 			|			 	 getBoundaryObject().bounceOffWall()
	 * 			|		dt = dt - timeTillBoundaryCollision()
	 * 			|		evolve(dt)
	 * @effect	If dt is smaller than the time till the first collide, all flyingObjects in the world 
	 * 			will move with dt.
	 * 			| if not (timeTillObjectCollision() <= dt || timeTillBoundaryCollision() <= dt)
	 * 			|	then for each flyingObject in flyingObjects:
	 * 			|			flyingObject.move(dt)
	 * @effect	If timerIsStarted is false, a new Timer will be set for this World.
	 * 			| this.startTimer()
	 */
	public void evolve(double dt, CollisionListener collisionListener) throws IllegalArgumentException{
		if (!timerIsStarted){
			startTimer();
		}
		if (timeTillObjectCollision() <= dt || timeTillBoundaryCollision() <= dt) {
			Double ttoc = timeTillObjectCollision();
			Double ttbc = timeTillBoundaryCollision();
			Double tc = dt;
			
			if (asteroids.Util.fuzzyEquals(ttoc, ttbc)) {
				if (ttoc < ttbc) {
					tc = ttoc; }
				else {
					tc = ttbc;
				}
                FlyingObject object = this.getFirstObjectCollision()[0];
                FlyingObject collisionObject = this.getFirstObjectCollision()[1];
                for(FlyingObject movingObject: flyingObjects) {
                        movingObject.move(tc);
                }                                
                getBoundaryObject().bounceOffWall();
                object.collide(collisionObject, collisionListener);                
			}
			else if(ttoc < ttbc) {
				FlyingObject object = this.getFirstObjectCollision()[0];
				FlyingObject collisionObject = this.getFirstObjectCollision()[1];
				tc = ttoc;
				for(FlyingObject movingObject: flyingObjects) {
					movingObject.move(tc);
				}						 
				object.collide(collisionObject, collisionListener);			 
			}			
			else if (ttoc > ttbc) {
				tc = ttbc;
				for(FlyingObject movingObject: flyingObjects) {
					movingObject.move(tc);
				}				 
				getBoundaryObject().bounceOffWall();
			}			
			dt=dt-tc;
			this.evolve(dt,collisionListener);
		}			
		else {
			for(FlyingObject movingObject: flyingObjects) {
				movingObject.move(dt);
			}
		}
	}
	
	/**
	 * Set a Timer to this world, with a delay of 200 milliseconds.
	 * 
	 * @post	A new Timer is Set to this world, with a delay of 200 milliseconds.
	 * 			| actionTimer = new Timer(200, getActionListener(getShipList())
	 * @post	timerIsStarted = true.
	 * 			| timerIsStarted = true
	 */
	@Model
	private void startTimer() {
		int delay = 200; //milliseconds
		this.actionTimer = new Timer(delay, getActionListener(getShipList()));
		actionTimer.start();
		timerIsStarted = true;
	}
	/**
	 * Returns a new anonymous class that implements ActionListener
	 * 
	 * @param shipList
	 * @return	ActionListener()
	 * 			a new anonymous class
	 */
	
	@Model
	private ActionListener getActionListener(final Set<Ship> shipList) {
		return new ActionListener() {
			
			@Override
			public void actionPerformed(ActionEvent e) {
				for(Ship programShip: shipList) {
					if(programShip.hasProgram()) {
						if(!programShip.isTerminated())
							programShip.getProgram().doNext(programShip);
					}
			  }				
			}			
		};
	}
	
	/**
	 * Return the time till the first object collides with a boundary.
	 */
	@Model
	private double timeTillBoundaryCollision() {
		double smallestTime = Double.MAX_VALUE;
		for(FlyingObject object1: flyingObjects) {
			if (object1.getTimeToCollision()<smallestTime) {
				smallestTime = object1.getTimeToCollision();
			}
		}
	return smallestTime;
	}
	
	/**
	 * Return the time till the first two objects collides with each other.
	 */	
	@Model
	private double timeTillObjectCollision() {
		double smallestTime = Double.MAX_VALUE;
		for(FlyingObject object1: flyingObjects) {
			for(FlyingObject object2: flyingObjects) {
				if (Util.fuzzyLessThanOrEqualTo(object1.getTimeToCollision(object2), smallestTime)) {
					boolean ownbullet = false;
					if (object1 instanceof Ship){
						ownbullet = ((Ship) object1).isBulletOff(object2);
					}
					else if (object2 instanceof Ship){
						ownbullet = ((Ship) object2).isBulletOff(object1);
					}
					if (ownbullet == false) {
						smallestTime = object1.getTimeToCollision(object2);
					}
					
				}
			}
		}
	return smallestTime;
	}
	
	/**
	 * return an array with the two objects that will collide first.
	 *
	 */
	@Model
	private FlyingObject[] getFirstObjectCollision() {
		FlyingObject[] collisionObjects = new FlyingObject[2];
		FlyingObject object = null;
		FlyingObject collisionObject = null;
		double smallestTime = Double.MAX_VALUE;
		for(FlyingObject object1: flyingObjects) {
			for(FlyingObject object2: flyingObjects) {
				if (Util.fuzzyLessThanOrEqualTo(object1.getTimeToCollision(object2), smallestTime)) {
					boolean ownbullet = false;
					if (object1 instanceof Ship){
						ownbullet = ((Ship) object1).isBulletOff(object2);
					}
					else if (object2 instanceof Ship){
						ownbullet = ((Ship) object2).isBulletOff(object1);
					}
					if (ownbullet == false) {
						smallestTime = object1.getTimeToCollision(object2);
						object = object1;
						collisionObject = object2;
					}
					
				}
			}
		}
		collisionObjects[0]=object;
		collisionObjects[1]=collisionObject;
		return collisionObjects;
	}
	/**
	 * Return the flyingObject that first collides with a wall.
	 */
	@Model
	private FlyingObject getBoundaryObject() {
		double smallestTime = Double.MAX_VALUE;
		FlyingObject boundaryObject = null;
		for(FlyingObject object1: flyingObjects) {
			if (object1.getTimeToCollision()<smallestTime) {
				smallestTime = object1.getTimeToCollision();
				boundaryObject = object1;
			}
		}
	return boundaryObject;
	}
	
		
	/**
	 * Return a list of all ships.
	 * 
	 * @return	A list of the flyingObjects in this world that are an instance of Ship.
	 * 			| for each flyingObject in flyingObjects:
	 * 			| 	if (flyingObject instanceof Ship)
	 * 			|		then ships.add(flyingObject)
	 * 			| result == ships
	 */
	public Set<Ship> getShipList() {
		Set<Ship> ships = new HashSet<Ship>();
		for(FlyingObject object: flyingObjects) {
			if (object instanceof Ship) {
				ships.add((Ship)object);
			}
		}
		return ships;
			
	}
	
	/**
	 * Return a list of all bullets.
	 * 
	 * @return	A list of the flyingObjects in this world that are an instance of Bullet.
	 * 			| for each flyingObject in flyingObjects:
	 * 			| 	if (flyingObject instanceof Bullet)
	 * 			|		then bullets.add(flyingObject)
	 * 			| result == bullets
	 */
	public Set<Bullet> getBulletList() {
		Set<Bullet> bullets = new HashSet<Bullet>();
		for(FlyingObject object: flyingObjects) {
			if (object instanceof Bullet) {
				bullets.add((Bullet)object);
			}
		}
		return bullets;
			
	}
	
	/**
	 * Return a list of all asteroids.
	 * 
	 * @return	A list of the flyingObjects in this world that are an instance of Asteroid.
	 * 			| for each flyingObject in flyingObjects:
	 * 			| 	if (flyingObject instanceof Asteroid)
	 * 			|		then asteroids.add(flyingObject)
	 * 			| result == asteroids
	 */
	public Set<Asteroid> getAsteroidList() {
		Set<Asteroid> asteroids = new HashSet<Asteroid>();
		for(FlyingObject object: flyingObjects) {
			if (object instanceof Asteroid) {
				asteroids.add((Asteroid)object);
			}
		}
		return asteroids;
			
	}
	
	/**
	 * Return a list of all flyingObjects.
	 * 
	 * @return	A list of the current flyingObjects of this world.
	 * 			| for each flyingObject in flyingObjects:
	 * 			|	returnObjects.add(flyingObject)
	 * 			| result == returnObjects
	 */
	public List<FlyingObject> getFlyingObjects() {
		List<FlyingObject> objects = new ArrayList<FlyingObject>();
		objects.addAll(flyingObjects);
		return objects;
	}
	
	/**
	 * Returns a set of all flyingObjects
	 * 
	 * @return	A set of the current flyingObjects in this world
	 * 			| for each flyingObject in flyingObjects:
	 * 			| 	returnObjects.add(flyingObject)
	 * 			| result == returnObjects
	 */
	public Set<FlyingObject> getFlyingObjectsSet() {
		Set<FlyingObject> objects = new HashSet<FlyingObject>();
		objects.addAll(flyingObjects);
		return objects;
	}
}